Міграції
========

> Note|Примітка: Міграції доступні з версії 1.1.6.

Як і початковий код, структура бази даних змінюється в процесі розробки та підтримки додатку.
Приміром, під час розробки може знадобитися додати нову таблицю або вже після розміщення додатка на сервері додати індекс або стовпець.
При цьому важливо відстежувати зміни в структурі бази даних (звані **міграціями**) також, як ми робимо це для нашого початкового коду.
Якщо початковий код і база даних не відповідають один одному, швидше за все все додаток не буде працювати.
Саме тому в Yii є підтримка міграцій, що дозволяє відстежувати зміни в базі даних, застосовувати міграції або відкочувати вже застосовані.

Нижче наведено поетапний процес як ми можемо використовувати міграції бази даних при розробці:

1. Олександр створює нову міграцію (наприклад, створює нову таблицю).
2. Олександр заливає її в систему контролю версій (SVN, GIT або іншу).
3. Павло оновлюється із системи контролю версій і отримує нову міграцію.
4. Павло застосовує міграцію до своєї локальної бази даних.

У Yii управління міграціями проводиться через консольну команду `yiic migrate`, яка підтримує створення нових міграцій,
застосування, відкат і повторне застосування міграцій, перегляд історії міграцій і нових міграцій.

> Note|Примітка: При роботі з командою `migrate` рекомендується використовувати yiic програми (тобто після `cd path/to/protected`),
а не yiic з директорії `framework`. Переконайтеся, що директорія `protected/migrations` існує і доступна для запису.
Також перевірте налаштування зʼєднання з базою даних в `protected/config/console.php`.

Створення міграцій
------------------

Для створення нової міграції (наприклад, яка створює таблицю для новин), ми повинні ввести в консолі:

~~~
yiic migrate create <name>
~~~

Обовʼязковий параметр `name` повинен містити дуже короткий опис міграції (таке, як, наприклад, `create_news_table`).
Як буде показано далі, цей параметр використовується як частина імені класу міграції,
тому використовувати можна тільки букви, цифри та знаки підкреслення.

~~~
yiic migrate create create_news_table
~~~

Наведена команда створить в директорії `protected/migrations` файл `m101129_185401_create_news_table.php`, який містить наступне:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
	}


    public function down()
    {
    	echo "m101129_185401_create_news_table does not support migration down.\n";
		return false;
    }

    /*
	// якщо потрібно виконати зміни в транзакції, використовуйте safeUp/safeDown
	// замість up/down
	public function safeUp()
	{
	}

	public function safeDown()
	{
	}
	*/
}
~~~

Варто відзначити, що імʼя класу збігається з іменем файлу і будується як `m<timestamp>_<name>`,
де `<timestamp>` — це час створення міграції в UTC (у форматі `yymmdd_hhmmss`), а `<name>` — те, що передано в параметрі `name` команди.

Метод `up()` повинен містити код, який виконує міграцію, а метод `down()` може містити код, який скасовує зроблене в `up()`.

Іноді реалізувати `down()` не виходить. Приміром, якщо в `up()` із таблиці видаляються дані, в `down()` повернути їх не вийде.
У цьому випадку міграція називається незворотною, що означає неможливість повернення до попереднього стану бази даних.
У наведеному вище коді метод `down()` повертає `false`. Це означає неможливість відкату міграції.

> Info|Інформація: Починаючи з версії 1.1.7, якщо метод `up()` або метод `down()` повертають `false`,
всі наступні міграції не будуть застосовані. В 1.1.6 для цього було необхідно викинути виняток.

Розглянемо як приклад міграцію, яка створює таблицю з новинами.

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
		$this->createTable('tbl_news', array(
			'id' => 'pk',
			'title' => 'string NOT NULL',
			'content' => 'text',
		));
	}

	public function down()
	{
		$this->dropTable('tbl_news');
	}
}
~~~

Базовий клас [CDbMigration] надає набір методів для роботи з даними і структурою бази даних.
Приміром, за допомогою [CDbMigration::createTable] можна створити нову таблицю, а [CDbMigration::insert] додасть рядок з даними.
Всі ці методи використовують підключення до бази даних, що повертає [CDbMigration::getDbConnection()],
що за замовчуванням еквівалентно `Yii::app()->db`.

> Info|Інформація: Як ви могли помітити, методи [CDbMigration] дуже схожі на методи [CDbCommand].
І це насправді так. Єдина відмінність у тому, що методи [CDbMigration] підраховують витрачений час
на їх виконання і виводять повідомлення про параметри методів.

Транзакційні міграції
---------------------

> Info|Інформація: Дана можливість підтримується починаючи з версії 1.1.7.

При застосуванні складних міграцій для дотримання цілісності та звʼязності потрібно або виконати всі запити, або,
якщо запит не виконався, скасувати попередні запити. Для досягнення цієї мети можна використовувати транзакції.

Можна почати транзакцію явно і заключити в неї весь код, який змінює базу даних:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
		$transaction=$this->getDbConnection()->beginTransaction();
		try
		{
			$this->createTable('tbl_news', array(
				'id' => 'pk',
				'title' => 'string NOT NULL',
				'content' => 'text',
			));
			$transaction->commit();
		}
		catch(Exception $e)
		{
			echo "Exception: ".$e->getMessage()."\n";
			$transaction->rollback();
			return false;
		}
	}

	// …схожий код для down()
}
~~~

А можна зробити це простіше, реалізувавши метод `safeUp()` замість `up()` і `safeDown()` замість `down()`:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function safeUp()
	{
		$this->createTable('tbl_news', array(
			'id' => 'pk',
			'title' => 'string NOT NULL',
			'content' => 'text',
		));
	}

	public function safeDown()
	{
		$this->dropTable('tbl_news');
	}
}
~~~

Yii при застосуванні міграції почне транзакцію і, потім, виконає код в `safeUp()` або `safeDown()`.
Якщо при цьому виникне яка-небудь помилка, відбудеться відкат транзакції, тобто база повернеться у початковий стан.

> Note|Примітка: Не всі СУБД повністю підтримують транзакції і не для всіх виразів. У тому випадку,
якщо підтримки транзакцій немає, реалізовувати треба `up()` і `down()`.
У разі MySQL та MariaDB, деякі вирази SQL можуть викликати неявне застосування транзакції
(детальніше про це у документації до [MySQL](http://dev.mysql.com/doc/refman/5.1/en/implicit-commit.html)
та [MariaDB](https://mariadb.com/kb/en/sql-statements-that-cause-an-implicit-commit/)).

Застосування міграцій
---------------------

Для того, щоб застосувати всі нові міграції (тобто привести локальну БД в актуальний стан), слід запустити наступну команду:

~~~
yiic migrate
~~~

Команда відобразить перелік всіх нових міграцій і, у випадку позитивної відповіді,
по черзі запустить метод `up()` у кожному класі міграції в порядку його створення.

Після застосування міграції у таблицю `tbl_migration` буде внесений відповідний запис.
Це дозволяє дізнатися, які міграції вже застосовані, а які ні. Якщо таблиця `tbl_migration` не існує,
вона буде створена автоматично в базі даних, зазначеної у компоненті `db` додатка.

Іноді потрібно застосувати лише одну або декілька нових міграцій. Для цього можна використовувати наступну команду:

~~~
yiic migrate up 3
~~~

При цьому застосовується три нові міграції. Замість трьох можна вказати будь-яку кількість застосовуваних міграцій.

Також можна привести стан бази даних до певної версії:

~~~
yiic migrate to 101129_185401
~~~

У якості параметра, що вказує версію, до якої потрібно привести базу даних, використовується частина імені файлу,
що відповідає часу створення міграції. Якщо між останньою застосованою і вказаною міграціями кілька міграцій,
то всі вони будуть застосовані. Якщо зазначена міграція вже застосовувалася, то буде проведений відкат всіх міграцій,
застосованих після неї (описано в наступному розділі).

Відкат міграцій
---------------

Для відкату однієї або декількох останніх застосованих міграцій можна скористатися наступною командою:

~~~
yiic migrate down [step]
~~~

де необовʼязковий параметр `step` задає кількість міграцій, які треба відкотити.
За замовчуванням відкочується одна остання застосована міграція.

Як було описано раніше, не всі міграції можна відкотити.
При спробі відкату таких міграцій буде викинуто виключення і процес відкату буде перерваний.

Повторне застосування міграцій
------------------------------

Повторне застосування міграції проводиться шляхом послідовного відкату і застосування.
Здійснити це можна наступною командою:

~~~
yiic migrate redo [step]
~~~

де необовʼязковий параметр `step` вказує кількість міграцій, які необхідно застосувати ще раз.
За замовчуванням повторюється одна остання міграція.

Перегляд інформації про міграції
--------------------------------

Крім застосування і відкату міграцій, інструмент міграцій може відображати історію міграцій і нові, ще не застосовані міграції.

~~~
yiic migrate history [limit]
yiic migrate new [limit]
~~~

Тут параметр `limit` вказує кількість міграцій, що відображаються. Якщо `limit` не вказаний, відображаються всі міграції.

Перша команда показує вже застосовані міграції, друга - міграції, які ще не були застосовані.

Зміна історії міграцій
----------------------

Іноді потрібно змінити історію міграцій так, щоб поточна версія була замінена на вказану без застосування або відкату міграцій.
Часто це потрібно при створенні нової міграції. Для цього можна використовувати наступну команду:

~~~
yiic migrate mark 101129_185401
~~~

Ця команда дуже схожа на `yiic migrate to`, але вона лише змінює таблицю історії міграцій до зазначеної
версії без застосування або відкату самих міграцій.

Налаштування команди міграцій
-----------------------------

Є кілька способів налаштувати команду міграцій.

### Використовуючи параметри командного рядка

Команда міграцій може бути налаштована чотирма опціями:

* `interactive`: чи використовувати інтерактивний режим. За замовчуванням true, тобто при застосуванні міграції буде виводитися підтвердження.
Якщо параметр виставлений у false, то міграції можна застосувати у фоновому режимі;

* `migrationPath`: вказує директорію, в якій зберігаються всі файли міграцій. Шлях повинен вказуватися у форматі псевдоніма
і відповідна йому директорія повинна існувати. Якщо параметр не вказаний,
буде використана піддиректорії `migrations`, що знаходиться всередині директорії з додатком;

* `migrationTable`: вказує імʼя таблиці в базі даних, яка зберігає історію міграцій. За замовчуванням воно дорівнює `tbl_migration`.
Структура таблиці наступна: `version varchar(255) primary key, apply_time integer`;

* `connectionID`: вказує ідентифікатор компонента бази даних. За умовчанням це 'db';

* `templateFile`: вказує шлях до файлу, який використовується як шаблон для генерації класів міграцій.
Шлях повинен вказуватися як псевдонім (тобто як `application.migrations.template`).
Якщо шлях не заданий, буде використовуватися внутрішній шаблон.
У шаблоні токен `{ClassName}` буде замінений імʼям класу міграції.

Для зазначення опцій використовується наступний формат:

~~~
yiic migrate up --option1=value1 --option2=value2 ...
~~~

Наприклад, якщо необхідно мігрувати модуль `forum`, файли міграцій якого розташовані у директорії модуля `migrations`,
можна скористатися наступною командою:

~~~
yiic migrate up --migrationPath=ext.forum.migrations
~~~

Варто відзначити, що при передачі через командний рядок прапорів,
таких як `interactive`, необхідно використовувати значення `1` або `0`:

~~~
yiic migrate --interactive=0
~~~

### Глобальна конфігурація команди

У той час, як опції командного рядка дозволяють нам на льоту конфігурувати команду міграцій,
іноді потрібно застосувати налаштування раз і назавжди.
Приміром, нам може знадобитися використовувати іншу таблицю для зберігання історії міграцій або використовувати свій шаблон міграції.
Це можна зробити, змінивши налаштування консольного додатку наступним чином:

~~~
[php]
return array(
	......
	'commandMap'=>array(
		'migrate'=>array(
			'class'=>'system.cli.commands.MigrateCommand',
			'migrationPath'=>'application.migrations',
			'migrationTable'=>'tbl_migration',
			'connectionID'=>'db',
			'templateFile'=>'application.migrations.template',
		),
		......
	),
	......
);
~~~

Тепер, при запуску команди `migrate`, зазначені вище налаштування будуть застосовані
без введення яких-небудь додаткових параметрів.